/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2002-2015, Open Source Geospatial Foundation (OSGeo)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 *
 *    This file is based on an origional contained in the GISToolkit project:
 *    http://gistoolkit.sourceforge.net/
 */
package org.geotools.data.shapefile.dbf;

import java.io.EOFException;
import java.io.IOException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.Charset;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geotools.util.NIOUtilities;
import org.geotools.util.logging.Logging;

/** Class to represent the header of a Dbase III file. Creation date: (5/15/2001 5:15:30 PM) */
public class DbaseFileHeader {
    // Constant for the size of a record
    private static final int FILE_DESCRIPTOR_SIZE = 32;

    // type of the file, must be 03h
    private static final byte MAGIC = 0x03;

    private static final int MINIMUM_HEADER = 33;

    // Date the file was last updated.
    private Date date = new Date();

    private int recordCnt = 0;

    private int fieldCnt = 0;

    // set this to a default length of 1, which is enough for one "space"
    // character which signifies an empty record
    private int recordLength = 1;

    // set this to a flagged value so if no fields are added before the write,
    // we know to adjust the headerLength to MINIMUM_HEADER
    private int headerLength = -1;

    private int largestFieldSize = 0;

    private static final Logger LOGGER = Logging.getLogger(DbaseFileHeader.class);

    /**
     * Returns the number of millis at January 1st 4713 BC
     *
     * <p>Calendar refCal = (Calendar) new GregorianCalendar(TimeZone.getTimeZone("UTC"));
     * refCal.set(Calendar.ERA, GregorianCalendar.BC); refCal.set(Calendar.YEAR, 4713);
     * refCal.set(Calendar.MONTH, Calendar.JANUARY); refCal.set(Calendar.DAY_OF_MONTH, 1);
     * refCal.set(Calendar.HOUR, 12); refCal.set(Calendar.MINUTE, 0); refCal.set(Calendar.SECOND,
     * 0); refCal.set(Calendar.MILLISECOND, 0); MILLIS_SINCE_4713 = refCal.getTimeInMillis() -
     * 43200000L; //(43200000L: 12 hour correction factor taken from DBFViewer2000)
     */
    public static long MILLIS_SINCE_4713 = -210866803200000L;

    /** Class for holding the information associated with a record. */
    class DbaseField {

        // Field Name
        String fieldName;

        // Field Type (C N L D @ or M)
        char fieldType;

        // Field Data Address offset from the start of the record.
        int fieldDataAddress;

        // Length of the data in bytes
        int fieldLength;

        // Field decimal count in Binary, indicating where the decimal is
        int decimalCount;
    }

    // Collection of header records.
    // lets start out with a zero-length array, just in case
    private DbaseField[] fields = new DbaseField[0];

    private Charset charset;

    public DbaseFileHeader() {
        this.charset = Charset.defaultCharset();
    }

    public DbaseFileHeader(Charset charset) {
        this.charset = charset;
    }

    private void read(ByteBuffer buffer, ReadableByteChannel channel) throws IOException {
        while (buffer.remaining() > 0) {
            if (channel.read(buffer) == -1) {
                throw new EOFException("Premature end of file");
            }
        }
    }

    /**
     * Determine the most appropriate Java Class for representing the data in the field.
     *
     * <PRE>
     * All packages are java.lang unless otherwise specified.
     * C (Character) -&gt; String
     * N (Numeric)   -&gt; Integer or Long or Double (depends on field's decimal count and fieldLength)
     * F (Floating)  -&gt; Double
     * L (Logical)   -&gt; Boolean
     * D (Date)      -&gt; java.util.Date (Without time)
     * @ (Timestamp) -&gt; java.sql.Timestamp (With time)
     * Unknown       -&gt; String
     * </PRE>
     *
     * @param i The index of the field, from 0 to <CODE>getNumFields() - 1</CODE> .
     * @return A Class which closely represents the dbase field type.
     */
    public Class<?> getFieldClass(int i) {
        Class<?> typeClass = null;

        switch (fields[i].fieldType) {
            case 'C':
                typeClass = String.class;
                break;

            case 'N':
                if (fields[i].decimalCount == 0) {
                    if (fields[i].fieldLength < 10) {
                        typeClass = Integer.class;
                    } else {
                        typeClass = Long.class;
                    }
                } else {
                    typeClass = Double.class;
                }
                break;

            case 'F':
                typeClass = Double.class;
                break;

            case 'L':
                typeClass = Boolean.class;
                break;

            case 'D':
                typeClass = Date.class;
                break;

            case '@':
                typeClass = Timestamp.class;
                break;

            default:
                typeClass = String.class;
                break;
        }

        return typeClass;
    }

    /**
     * Add a column to this DbaseFileHeader. The type is one of (C N L or D) character, number,
     * logical(true/false), or date. The Field length is the total length in bytes reserved for this
     * column. The decimal count only applies to numbers(N), and floating point values (F), and
     * refers to the number of characters to reserve after the decimal point. <B>Don't expect
     * miracles from this...</B>
     *
     * <PRE>
     * Field Type MaxLength
     * ---------- ---------
     * C          254
     * D          8
     * @          8
     * F          20
     * N          18
     * </PRE>
     *
     * @param inFieldName The name of the new field, must be less than 10 characters or it gets
     *     truncated.
     * @param inFieldType A character representing the dBase field, ( see above ). Case insensitive.
     * @param inFieldLength The length of the field, in bytes ( see above )
     * @param inDecimalCount For numeric fields, the number of decimal places to track.
     * @throws DbaseFileException If the type is not recognized.
     */
    public void addColumn(
            String inFieldName, char inFieldType, int inFieldLength, int inDecimalCount)
            throws DbaseFileException {
        if (inFieldLength <= 0) {
            throw new DbaseFileException("field length <= 0");
        }
        if (fields == null) {
            fields = new DbaseField[0];
        }
        int tempLength = 1; // the length is used for the offset, and there is a
        // * for deleted as the first byte
        DbaseField[] tempFieldDescriptors = new DbaseField[fields.length + 1];
        for (int i = 0; i < fields.length; i++) {
            fields[i].fieldDataAddress = tempLength;
            tempLength = tempLength + fields[i].fieldLength;
            tempFieldDescriptors[i] = fields[i];
        }
        tempFieldDescriptors[fields.length] = new DbaseField();
        tempFieldDescriptors[fields.length].fieldLength = inFieldLength;
        tempFieldDescriptors[fields.length].decimalCount = inDecimalCount;
        tempFieldDescriptors[fields.length].fieldDataAddress = tempLength;

        // set the field name
        String tempFieldName = inFieldName;
        if (tempFieldName == null) {
            tempFieldName = "NoName";
        }
        // Fix for GEOT-42, ArcExplorer will not handle field names > 10 chars
        // Sorry folks.
        String fitFieldName = fitStringRegardingCharset(tempFieldName, 10);
        if (!tempFieldName.equals(fitFieldName)) {
            tempFieldName = fitFieldName;
            if (LOGGER.isLoggable(Level.WARNING)) {
                LOGGER.warning(
                        "FieldName "
                                + inFieldName
                                + " is longer than 10 bytes in given charset, truncating to "
                                + tempFieldName);
            }
        }
        tempFieldDescriptors[fields.length].fieldName = tempFieldName;

        // the field type
        if ((inFieldType == 'C') || (inFieldType == 'c')) {
            tempFieldDescriptors[fields.length].fieldType = 'C';
            if (inFieldLength > 254) {
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.fine(
                            "Field Length for "
                                    + inFieldName
                                    + " set to "
                                    + inFieldLength
                                    + " Which is longer than 254, not consistent with dbase III");
                }
            }
        } else if ((inFieldType == 'S') || (inFieldType == 's')) {
            tempFieldDescriptors[fields.length].fieldType = 'C';
            if (LOGGER.isLoggable(Level.WARNING)) {
                LOGGER.warning(
                        "Field type for "
                                + inFieldName
                                + " set to S which is flat out wrong people!, I am setting this to C, in the hopes you meant character.");
            }
            if (inFieldLength > 254) {
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.fine(
                            "Field Length for "
                                    + inFieldName
                                    + " set to "
                                    + inFieldLength
                                    + " Which is longer than 254, not consistent with dbase III");
                }
            }
            tempFieldDescriptors[fields.length].fieldLength = 8;
        } else if ((inFieldType == 'D') || (inFieldType == 'd')) {
            tempFieldDescriptors[fields.length].fieldType = 'D';
            if (inFieldLength != 8) {
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.fine(
                            "Field Length for "
                                    + inFieldName
                                    + " set to "
                                    + inFieldLength
                                    + " Setting to 8 digits YYYYMMDD");
                }
            }
            tempFieldDescriptors[fields.length].fieldLength = 8;
        } else if (inFieldType == '@') {
            tempFieldDescriptors[fields.length].fieldType = '@';
            if (inFieldLength != 8) {
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.fine(
                            "Field Length for "
                                    + inFieldName
                                    + " set to "
                                    + inFieldLength
                                    + " Setting to 8 digits - two longs,"
                                    + "one long for date and one long for time");
                }
            }
            tempFieldDescriptors[fields.length].fieldLength = 8;
        } else if ((inFieldType == 'F') || (inFieldType == 'f')) {
            tempFieldDescriptors[fields.length].fieldType = 'F';
            if (inFieldLength > 20) {
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.fine(
                            "Field Length for "
                                    + inFieldName
                                    + " set to "
                                    + inFieldLength
                                    + " Preserving length, but should be set to Max of 20 not valid for dbase IV, and UP specification, not present in dbaseIII.");
                }
            }
        } else if ((inFieldType == 'N') || (inFieldType == 'n')) {
            tempFieldDescriptors[fields.length].fieldType = 'N';
            if (inFieldLength > 18) {
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.fine(
                            "Field Length for "
                                    + inFieldName
                                    + " set to "
                                    + inFieldLength
                                    + " Preserving length, but should be set to Max of 18 for dbase III specification.");
                }
            }
            if (inDecimalCount < 0) {
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.fine(
                            "Field Decimal Position for "
                                    + inFieldName
                                    + " set to "
                                    + inDecimalCount
                                    + " Setting to 0 no decimal data will be saved.");
                }
                tempFieldDescriptors[fields.length].decimalCount = 0;
            }
            if (inDecimalCount > inFieldLength - 1) {
                if (LOGGER.isLoggable(Level.WARNING)) {
                    LOGGER.warning(
                            "Field Decimal Position for "
                                    + inFieldName
                                    + " set to "
                                    + inDecimalCount
                                    + " Setting to "
                                    + (inFieldLength - 1)
                                    + " no non decimal data will be saved.");
                }
                tempFieldDescriptors[fields.length].decimalCount = inFieldLength - 1;
            }
        } else if ((inFieldType == 'L') || (inFieldType == 'l')) {
            tempFieldDescriptors[fields.length].fieldType = 'L';
            if (inFieldLength != 1) {
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.fine(
                            "Field Length for "
                                    + inFieldName
                                    + " set to "
                                    + inFieldLength
                                    + " Setting to length of 1 for logical fields.");
                }
            }
            tempFieldDescriptors[fields.length].fieldLength = 1;
        } else {
            throw new DbaseFileException(
                    "Undefined field type " + inFieldType + " For column " + inFieldName);
        }
        // the length of a record
        tempLength = tempLength + tempFieldDescriptors[fields.length].fieldLength;

        // set the new fields.
        fields = tempFieldDescriptors;
        fieldCnt = fields.length;
        headerLength = MINIMUM_HEADER + 32 * fields.length;
        recordLength = tempLength;
    }

    private String fitStringRegardingCharset(String s, int bytesCnt) {
        int len = s.length();
        String result = s;
        while (result.getBytes(charset).length > bytesCnt) {
            result = s.substring(0, --len);
        }
        return result;
    }

    /**
     * Remove a column from this DbaseFileHeader.
     *
     * @todo This is really ugly, don't know who wrote it, but it needs fixing...
     * @param inFieldName The name of the field, will ignore case and trim.
     * @return index of the removed column, -1 if no found
     */
    public int removeColumn(String inFieldName) {

        int retCol = -1;
        int tempLength = 1;
        DbaseField[] tempFieldDescriptors = new DbaseField[fields.length - 1];
        for (int i = 0, j = 0; i < fields.length; i++) {
            if (!inFieldName.equalsIgnoreCase(fields[i].fieldName.trim())) {
                // if this is the last field and we still haven't found the
                // named field
                if (i == j && i == fields.length - 1) {
                    LOGGER.warning(
                            "Could not find a field named '" + inFieldName + "' for removal");
                    return retCol;
                }
                tempFieldDescriptors[j] = fields[i];
                tempFieldDescriptors[j].fieldDataAddress = tempLength;
                tempLength += tempFieldDescriptors[j].fieldLength;
                // only increment j on non-matching fields
                j++;
            } else {
                retCol = i;
            }
        }

        // set the new fields.
        fields = tempFieldDescriptors;
        headerLength = 33 + 32 * fields.length;
        recordLength = tempLength;

        return retCol;
    }

    // Retrieve the length of the field at the given index
    /**
     * Returns the field length in bytes.
     *
     * @param inIndex The field index.
     * @return The length in bytes.
     */
    public int getFieldLength(int inIndex) {
        return fields[inIndex].fieldLength;
    }

    // Retrieve the location of the decimal point within the field.
    /**
     * Get the decimal count of this field.
     *
     * @param inIndex The field index.
     * @return The decimal count.
     */
    public int getFieldDecimalCount(int inIndex) {
        return fields[inIndex].decimalCount;
    }

    // Retrieve the Name of the field at the given index
    /**
     * Get the field name.
     *
     * @param inIndex The field index.
     * @return The name of the field.
     */
    public String getFieldName(int inIndex) {
        return fields[inIndex].fieldName;
    }

    // Retrieve the type of field at the given index
    /**
     * Get the character class of the field.
     *
     * @param inIndex The field index.
     * @return The dbase character representing this field.
     */
    public char getFieldType(int inIndex) {
        return fields[inIndex].fieldType;
    }

    /**
     * Get the date this file was last updated.
     *
     * @return The Date last modified.
     */
    public Date getLastUpdateDate() {
        return date;
    }

    /**
     * Return the number of fields in the records.
     *
     * @return The number of fields in this table.
     */
    public int getNumFields() {
        return fields.length;
    }

    /**
     * Return the number of records in the file
     *
     * @return The number of records in this table.
     */
    public int getNumRecords() {
        return recordCnt;
    }

    /**
     * Get the length of the records in bytes.
     *
     * @return The number of bytes per record.
     */
    public int getRecordLength() {
        return recordLength;
    }

    /**
     * Get the length of the header
     *
     * @return The length of the header in bytes.
     */
    public int getHeaderLength() {
        return headerLength;
    }

    /**
     * Read the header data from the DBF file.
     *
     * @param channel A readable byte channel. If you have an InputStream you need to use, you can
     *     call java.nio.Channels.getChannel(InputStream in).
     * @throws IOException If errors occur while reading.
     */
    public void readHeader(ReadableByteChannel channel) throws IOException {
        // we'll read in chunks of 1K
        ByteBuffer in = NIOUtilities.allocate(1024);
        try {
            // do this or GO CRAZY
            // ByteBuffers come preset to BIG_ENDIAN !
            in.order(ByteOrder.LITTLE_ENDIAN);

            // only want to read first 10 bytes...
            ((Buffer) in).limit(10);

            read(in, channel);
            ((Buffer) in).position(0);

            // type of file.
            byte magic = in.get();
            if (magic != MAGIC) {
                throw new IOException("Unsupported DBF file Type " + Integer.toHexString(magic));
            }

            // parse the update date information.
            int tempUpdateYear = in.get();
            int tempUpdateMonth = in.get();
            int tempUpdateDay = in.get();
            // ouch Y2K uncompliant
            if (tempUpdateYear > 90) {
                tempUpdateYear = tempUpdateYear + 1900;
            } else {
                tempUpdateYear = tempUpdateYear + 2000;
            }
            Calendar c = Calendar.getInstance();
            c.set(Calendar.YEAR, tempUpdateYear);
            c.set(Calendar.MONTH, tempUpdateMonth - 1);
            c.set(Calendar.DATE, tempUpdateDay);
            date = c.getTime();

            // read the number of records.
            recordCnt = in.getInt();

            // read the length of the header structure.
            // ahhh.. unsigned little-endian shorts
            // mask out the byte and or it with shifted 2nd byte
            headerLength = (in.get() & 0xff) | ((in.get() & 0xff) << 8);

            // if the header is bigger than our 1K, reallocate
            if (headerLength > in.capacity()) {
                NIOUtilities.clean(in, false);
                in = NIOUtilities.allocate(headerLength - 10);
            }
            ((Buffer) in).limit(headerLength - 10);
            in.position(0);
            read(in, channel);
            in.position(0);

            // read the length of a record
            // ahhh.. unsigned little-endian shorts
            recordLength = (in.get() & 0xff) | ((in.get() & 0xff) << 8);

            // skip the reserved bytes in the header.
            in.position(in.position() + 20);

            // calculate the number of Fields in the header
            fieldCnt = (headerLength - FILE_DESCRIPTOR_SIZE - 1) / FILE_DESCRIPTOR_SIZE;

            // read all of the header records
            List<DbaseField> lfields = new ArrayList<>();
            for (int i = 0; i < fieldCnt; i++) {
                DbaseField field = new DbaseField();

                // read the field name
                byte[] buffer = new byte[11];
                in.get(buffer);
                String name = new String(buffer, charset.name());
                int nullPoint = name.indexOf(0);
                if (nullPoint != -1) {
                    name = name.substring(0, nullPoint);
                }
                field.fieldName = name.trim();

                // read the field type
                field.fieldType = (char) in.get();

                // read the field data address, offset from the start of the record.
                field.fieldDataAddress = in.getInt();

                // read the field length in bytes
                int length = in.get();
                if (length < 0) {
                    length = length + 256;
                }
                field.fieldLength = length;

                if (length > largestFieldSize) {
                    largestFieldSize = length;
                }

                // read the field decimal count in bytes
                field.decimalCount = in.get();

                // reserved bytes.
                // in.skipBytes(14);
                in.position(in.position() + 14);

                // some broken shapefiles have 0-length attributes. The reference
                // implementation
                // (ArcExplorer 2.0, built with MapObjects) just ignores them.
                if (field.fieldLength > 0) {
                    lfields.add(field);
                }
            }

            // Last byte is a marker for the end of the field definitions.
            // in.skipBytes(1);
            in.position(in.position() + 1);

            fields = new DbaseField[lfields.size()];
            fields = lfields.toArray(fields);
        } finally {
            NIOUtilities.clean(in, false);
        }
    }

    /**
     * Read the header data from the DBF file.
     *
     * @param in The ByteBuffer to read the header from
     * @throws IOException If errors occur while reading.
     */
    public void readHeader(ByteBuffer in) throws IOException {
        // do this or GO CRAZY
        // ByteBuffers come preset to BIG_ENDIAN !
        in.order(ByteOrder.LITTLE_ENDIAN);

        // type of file.
        byte magic = in.get();
        if (magic != MAGIC) {
            throw new IOException("Unsupported DBF file Type " + Integer.toHexString(magic));
        }

        // parse the update date information.
        int tempUpdateYear = in.get();
        int tempUpdateMonth = in.get();
        int tempUpdateDay = in.get();
        // ouch Y2K uncompliant
        if (tempUpdateYear > 90) {
            tempUpdateYear = tempUpdateYear + 1900;
        } else {
            tempUpdateYear = tempUpdateYear + 2000;
        }
        Calendar c = Calendar.getInstance();
        c.set(Calendar.YEAR, tempUpdateYear);
        c.set(Calendar.MONTH, tempUpdateMonth - 1);
        c.set(Calendar.DATE, tempUpdateDay);
        date = c.getTime();

        // read the number of records.
        recordCnt = in.getInt();

        // read the length of the header structure.
        // ahhh.. unsigned little-endian shorts
        // mask out the byte and or it with shifted 2nd byte
        headerLength = (in.get() & 0xff) | ((in.get() & 0xff) << 8);

        // if the header is bigger than our 1K, reallocate
        if (headerLength > in.capacity()) {
            throw new IllegalArgumentException(
                    "The contract says the buffer should be long enough to fit all the header!");
        }

        // read the length of a record
        // ahhh.. unsigned little-endian shorts
        recordLength = (in.get() & 0xff) | ((in.get() & 0xff) << 8);

        // skip the reserved bytes in the header.
        in.position(in.position() + 20);

        // calculate the number of Fields in the header
        fieldCnt = (headerLength - FILE_DESCRIPTOR_SIZE - 1) / FILE_DESCRIPTOR_SIZE;

        // read all of the header records
        List<DbaseField> lfields = new ArrayList<>();
        for (int i = 0; i < fieldCnt; i++) {
            DbaseField field = new DbaseField();

            // read the field name
            byte[] buffer = new byte[11];
            in.get(buffer);
            String name = new String(buffer, charset.name());
            int nullPoint = name.indexOf(0);
            if (nullPoint != -1) {
                name = name.substring(0, nullPoint);
            }
            field.fieldName = name.trim();

            // read the field type
            field.fieldType = (char) in.get();

            // read the field data address, offset from the start of the record.
            field.fieldDataAddress = in.getInt();

            // read the field length in bytes
            int length = in.get();
            if (length < 0) {
                length = length + 256;
            }
            field.fieldLength = length;

            if (length > largestFieldSize) {
                largestFieldSize = length;
            }

            // read the field decimal count in bytes
            field.decimalCount = in.get();

            // reserved bytes.
            // in.skipBytes(14);
            in.position(in.position() + 14);

            // some broken shapefiles have 0-length attributes. The reference
            // implementation
            // (ArcExplorer 2.0, built with MapObjects) just ignores them.
            if (field.fieldLength > 0) {
                lfields.add(field);
            }
        }

        // Last byte is a marker for the end of the field definitions.
        // in.skipBytes(1);
        in.position(in.position() + 1);

        fields = new DbaseField[lfields.size()];
        fields = lfields.toArray(fields);
    }

    /**
     * Get the largest field size of this table.
     *
     * @return The largest field size in bytes.
     */
    public int getLargestFieldSize() {
        return largestFieldSize;
    }

    /**
     * Set the number of records in the file
     *
     * @param inNumRecords The number of records.
     */
    public void setNumRecords(int inNumRecords) {
        recordCnt = inNumRecords;
    }

    /**
     * Write the header data to the DBF file.
     *
     * @param out A channel to write to. If you have an OutputStream you can obtain the correct
     *     channel by using java.nio.Channels.newChannel(OutputStream out).
     * @throws IOException If errors occur.
     */
    public void writeHeader(WritableByteChannel out) throws IOException {
        // take care of the annoying case where no records have been added...
        if (headerLength == -1) {
            headerLength = MINIMUM_HEADER;
        }
        ByteBuffer buffer = NIOUtilities.allocate(headerLength);
        try {
            buffer.order(ByteOrder.LITTLE_ENDIAN);

            // write the output file type.
            buffer.put(MAGIC);

            // write the date stuff
            Calendar c = Calendar.getInstance();
            c.setTime(new Date());
            buffer.put((byte) (c.get(Calendar.YEAR) % 100));
            buffer.put((byte) (c.get(Calendar.MONTH) + 1));
            buffer.put((byte) (c.get(Calendar.DAY_OF_MONTH)));

            // write the number of records in the datafile.
            buffer.putInt(recordCnt);

            // write the length of the header structure.
            buffer.putShort((short) headerLength);

            // write the length of a record
            buffer.putShort((short) recordLength);

            // // write the reserved bytes in the header
            // for (int i=0; i<20; i++) out.writeByteLE(0);
            buffer.position(buffer.position() + 20);

            // write all of the header records
            int tempOffset = 0;
            for (DbaseField field : fields) {

                // write the field name
                byte[] fieldNameBytes = field.fieldName.getBytes(charset);
                for (int j = 0; j < 11; j++) {
                    if (j < fieldNameBytes.length) {
                        buffer.put(fieldNameBytes[j]);
                    } else {
                        buffer.put((byte) 0);
                    }
                }

                // write the field type
                buffer.put((byte) field.fieldType);
                // // write the field data address, offset from the start of the
                // record.
                buffer.putInt(tempOffset);
                tempOffset += field.fieldLength;

                // write the length of the field.
                buffer.put((byte) field.fieldLength);

                // write the decimal count.
                buffer.put((byte) field.decimalCount);

                // write the reserved bytes.
                // for (in j=0; jj<14; j++) out.writeByteLE(0);
                buffer.position(buffer.position() + 14);
            }

            // write the end of the field definitions marker
            buffer.put((byte) 0x0D);

            buffer.position(0);

            int r = buffer.remaining();
            while ((r -= out.write(buffer)) > 0) {; // do nothing
            }
        } finally {
            NIOUtilities.clean(buffer, false);
        }
    }

    /**
     * Get a simple representation of this header.
     *
     * @return A String representing the state of the header.
     */
    @Override
    public String toString() {
        StringBuffer fs = new StringBuffer();
        for (DbaseField f : fields) {
            fs.append(
                    f.fieldName
                            + " "
                            + f.fieldType
                            + " "
                            + f.fieldLength
                            + " "
                            + f.decimalCount
                            + " "
                            + f.fieldDataAddress
                            + "\n");
        }

        return "DB3 Header\n"
                + "Date : "
                + date
                + "\n"
                + "Records : "
                + recordCnt
                + "\n"
                + "Fields : "
                + fieldCnt
                + "\n"
                + fs;
    }

    /** Returns the expected file size for the given number of records in the file */
    public long getLengthForRecords(int records) {
        return headerLength + records * recordLength;
    }
}
